const { forEach } = require("lodash");
const detect = require("detect-port-alt");
const chalk = require("chalk");
const isRoot = require("is-root");
const url = require("url");
const fs = require("fs");
const address = require("address");
const path = require("path");
const paths = require("../config/paths");

function resolveLoopback(proxy) {
  const o = url.parse(proxy);
  o.host = undefined;
  if (o.hostname !== "localhost") {
    return proxy;
  }
  // Unfortunately, many languages (unlike node) do not yet support IPv6.
  // This means even though localhost resolves to ::1, the application
  // must fall back to IPv4 (on 127.0.0.1).
  // We can re-enable this in a few years.
  /*try {
    o.hostname = address.ipv6() ? '::1' : '127.0.0.1';
  } catch (_ignored) {
    o.hostname = '127.0.0.1';
  }*/

  try {
    // Check if we're on a network; if we are, chances are we can resolve
    // localhost. Otherwise, we can just be safe and assume localhost is
    // IPv4 for maximum compatibility.
    if (!address.ip()) {
      o.hostname = "127.0.0.1";
    }
  } catch (_ignored) {
    o.hostname = "127.0.0.1";
  }
  return url.format(o);
}

// We need to provide a custom onError function for httpProxyMiddleware.
// It allows us to log custom error messages on the console.
function onProxyError(proxy) {
  return (err, req, res) => {
    const host = req.headers && req.headers.host;
    console.log(
      chalk.red("Proxy error:") +
        " Could not proxy request " +
        chalk.cyan(req.url) +
        " from " +
        chalk.cyan(host) +
        " to " +
        chalk.cyan(proxy) +
        "."
    );
    console.log(
      "See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (" +
        chalk.cyan(err.code) +
        ")."
    );
    console.log();

    // And immediately send the proper error response to the client.
    // Otherwise, the request will eventually timeout with ERR_EMPTY_RESPONSE on the client side.
    if (res.writeHead && !res.headersSent) {
      res.writeHead(500);
    }
    res.end(
      "Proxy error: Could not proxy request " +
        req.url +
        " from " +
        host +
        " to " +
        proxy +
        " (" +
        err.code +
        ")."
    );
  };
}

function prepareProxy(proxy, appPublicFolder) {
  // `proxy` lets you specify alternate servers for specific requests.
  // It can either be a string or an object conforming to the Webpack dev server proxy configuration
  // https://webpack.github.io/docs/webpack-dev-server.html
  if (!proxy) {
    return undefined;
  }
  if (typeof proxy !== "object" && typeof proxy !== "string") {
    console.log(
      chalk.red('When specified, "proxy" in package.json must be a string or an object.')
    );
    console.log(chalk.red('Instead, the type of "proxy" was "' + typeof proxy + '".'));
    console.log(chalk.red('Either remove "proxy" from package.json, or make it an object.'));
    process.exit(1);
  }

  // Otherwise, if proxy is specified, we will let it handle any request except for files in the public folder.
  function mayProxy(pathname) {
    const maybePublicPath = path.resolve(appPublicFolder, pathname.slice(1));
    return !fs.existsSync(maybePublicPath);
  }

  // Support proxy as a string for those who are using the simple proxy option
  if (typeof proxy === "string") {
    if (!/^http(s)?:\/\//.test(proxy)) {
      console.log(
        chalk.red(
          'When "proxy" is specified in package.json it must start with either http:// or https://'
        )
      );
      process.exit(1);
    }

    let target;
    if (process.platform === "win32") {
      target = resolveLoopback(proxy);
    } else {
      target = proxy;
    }
    return [
      {
        target,
        logLevel: "silent",
        // For single page apps, we generally want to fallback to /index.html.
        // However we also want to respect `proxy` for API calls.
        // So if `proxy` is specified as a string, we need to decide which fallback to use.
        // We use a heuristic: We want to proxy all the requests that are not meant
        // for static assets and as all the requests for static assets will be using
        // `GET` method, we can proxy all non-`GET` requests.
        // For `GET` requests, if request `accept`s text/html, we pick /index.html.
        // Modern browsers include text/html into `accept` header when navigating.
        // However API calls like `fetch()` won’t generally accept text/html.
        // If this heuristic doesn’t work well for you, use a custom `proxy` object.
        context: function(pathname, req) {
          return (
            req.method !== "GET" ||
            (mayProxy(pathname) &&
              req.headers.accept &&
              req.headers.accept.indexOf("text/html") === -1)
          );
        },
        onProxyReq: proxyReq => {
          // Browers may send Origin headers even with same-origin
          // requests. To prevent CORS issues, we have to change
          // the Origin to match the target URL.
          if (proxyReq.getHeader("origin")) {
            proxyReq.setHeader("origin", target);
          }
        },
        router: function(req) {
          return target ? target : req.headers.domain;
        },
        bypass: function(req, res, proxyOptions) {
          const publicPath = paths.publicPath || "/";
          const tarUrl = req.url.indexOf(publicPath) !== -1;
          if (tarUrl) {
            var tarContentArr = ["html", "css", "javascript"];
            const tarContent = tarContentArr.find(item => {
              return req.headers.accept.indexOf(item) !== -1;
            });
            if (tarContent) {
              console.log("Skipping proxy for browser request.");
              return tarContent;
            }
          }
        },
        onError: onProxyError(target),
        secure: false,
        changeOrigin: true,
        ws: true,
        xfwd: true
      }
    ];
  }

  // Otherwise, proxy is an object so create an array of proxies to pass to webpackDevServer
  return Object.keys(proxy).map(function(context) {
    if (!proxy[context].hasOwnProperty("target")) {
      console.log(
        chalk.red(
          "When `proxy` in package.json is as an object, each `context` object must have a " +
            "`target` property specified as a url string"
        )
      );
      process.exit(1);
    }
    let target;
    if (process.platform === "win32") {
      target = resolveLoopback(proxy[context].target);
    } else {
      target = proxy[context].target;
    }
    let url = target;
    return Object.assign({}, proxy[context], {
      context: function(pathname) {
        return mayProxy(pathname) && pathname.match(context);
      },
      onProxyReq: proxyReq => {
        // Browers may send Origin headers even with same-origin
        // requests. To prevent CORS issues, we have to change
        // the Origin to match the target URL.
        if (proxyReq.getHeader("origin")) {
          proxyReq.setHeader("origin", target);
        }
      },
      pathRewrite: (path, req) => {
        return path;
      },
      router: function(req) {
        return url;
      },
      bypass: function(req, res, proxyOptions) {
        if (req.headers.domain) {
          url = req.headers.domain;
        }
        const tarUrl = req.url.indexOf("/static/") !== -1;
        const tarHtml = "accept" in req.headers && req.headers.accept.indexOf("html") !== -1;
        if (tarUrl || tarHtml) {
          console.log("Skipping proxy for browser request:" + req.url);
          return req.url;
        }
      },
      target,
      _context_: context,
      onError: onProxyError(target)
    });
  });
}
function prepareUrls(protocol, host, port) {
  const formatUrl = hostname =>
    url.format({
      protocol,
      hostname,
      port,
      pathname: "/"
    });
  const prettyPrintUrl = hostname =>
    url.format({
      protocol,
      hostname,
      port: chalk.bold(port),
      pathname: "/"
    });

  const isUnspecifiedHost = host === "0.0.0.0" || host === "::";
  let prettyHost, lanUrlForConfig, lanUrlForTerminal;
  if (isUnspecifiedHost) {
    prettyHost = "localhost";
    try {
      // This can only return an IPv4 address
      lanUrlForConfig = address.ip();
      if (lanUrlForConfig) {
        // Check if the address is a private ip
        // https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
        if (/^10[.]|^172[.](1[6-9]|2[0-9]|3[0-1])[.]|^192[.]168[.]/.test(lanUrlForConfig)) {
          // Address is private, format it for later use
          lanUrlForTerminal = prettyPrintUrl(lanUrlForConfig);
        } else {
          // Address is not private, so we will discard it
          lanUrlForConfig = undefined;
        }
      }
    } catch (_e) {
      // ignored
    }
  } else {
    prettyHost = host;
  }
  const localUrlForTerminal = prettyPrintUrl(prettyHost);
  const localUrlForBrowser = formatUrl(prettyHost);
  return {
    lanUrlForConfig,
    lanUrlForTerminal,
    localUrlForTerminal,
    localUrlForBrowser
  };
}
function choosePort(host, defaultPort) {
  return detect(defaultPort, host).then(
    port =>
      new Promise(resolve => {
        if (port === defaultPort) {
          return resolve(port);
        }
        const message =
          process.platform !== "win32" && defaultPort < 1024 && !isRoot()
            ? `Admin permissions are required to run a server on a port below 1024.`
            : `Something is already running on port ${defaultPort}.`;

        console.log(chalk.red(message));
        resolve(null);
        // if (isInteractive) {
        //   clearConsole();
        //   const existingProcess = getProcessForPort(defaultPort);
        //   const question = {
        //     type: 'confirm',
        //     name: 'shouldChangePort',
        //     message:
        //       chalk.yellow(
        //         message +
        //           `${existingProcess ? ` Probably:\n  ${existingProcess}` : ''}`
        //       ) + '\n\nWould you like to run the app on another port instead?',
        //     default: true,
        //   };
        //   inquirer.prompt(question).then(answer => {
        //     if (answer.shouldChangePort) {
        //       resolve(port);
        //     } else {
        //       resolve(null);
        //     }
        //   });
        // } else {
        //   console.log(chalk.red(message));
        //   resolve(null);
        // }
      }),
    err => {
      throw new Error(
        chalk.red(`Could not find an open port at ${chalk.bold(host)}.`) +
          "\n" +
          ("Network error message: " + err.message || err) +
          "\n"
      );
    }
  );
}
function createCompiler(webpack, config, appName, urls, useYarn, proxyConfig) {
  // "Compiler" is a low-level interface to Webpack.
  // It lets us listen to some events and provide our own custom messages.
  let compiler;
  try {
    compiler = webpack(config);
  } catch (err) {
    console.log(chalk.red("Failed to compile."));
    console.log();
    console.log(err.message || err);
    console.log();
    process.exit(1);
  }

  // "invalid" event fires when you have changed a file, and Webpack is
  // recompiling a bundle. WebpackDevServer takes care to pause serving the
  // bundle, so if you refresh, it'll wait instead of serving the old one.
  // "invalid" is short for "bundle invalidated", it doesn't imply any errors.
  compiler.plugin("invalid", () => {
    // if (isInteractive) {
    //   clearConsole();
    // }
    console.log("Compiling...");
  });

  // let isFirstCompile = true;

  // "done" event fires when Webpack has finished recompiling the bundle.
  // Whether or not you have warnings or errors, you will get this event.
  compiler.plugin("done", stats => {
    // if (isInteractive) {
    //   clearConsole();
    // }

    // We have switched off the default Webpack output in WebpackDevServer
    // options so we are going to "massage" the warnings and errors and present
    // them in a readable focused way.
    const messages = stats.toJson({}, true);
    const isSuccessful = !messages.errors.length && !messages.warnings.length;
    if (isSuccessful) {
      console.log(chalk.green("Compiled successfully!"));
    }
    // if (isSuccessful && (isInteractive || isFirstCompile)) {
    //   printInstructions(appName, urls, useYarn, proxyConfig);
    // }
    // isFirstCompile = false;

    // If errors exist, only show errors.
    if (messages.errors.length) {
      // Only keep the first error. Others are often indicative
      // of the same problem, but confuse the reader with noise.
      if (messages.errors.length > 1) {
        messages.errors.length = 1;
      }
      console.log(chalk.red("Failed to compile.\n"));
      console.log(messages.errors.join("\n\n"));
      return;
    }

    // Show warnings if no errors were found.
    if (messages.warnings.length) {
      console.log(chalk.yellow("Compiled with warnings.\n"));
      console.log(messages.warnings.join("\n\n"));

      // Teach some ESLint tricks.
      console.log(
        "\nSearch for the " +
          chalk.underline(chalk.yellow("keywords")) +
          " to learn more about each warning."
      );
      console.log(
        "To ignore, add " + chalk.cyan("// eslint-disable-next-line") + " to the line before.\n"
      );
    }
  });
  return compiler;
}
module.exports = {
  choosePort,
  createCompiler,
  prepareUrls,
  prepareProxy
};
